Skip to content

New data-tooluse AI SDK packet and Tool Approvals Implemented#2407

Merged
sawka merged 9 commits intomainfrom
sawka/tool-approval
Oct 9, 2025
Merged

New data-tooluse AI SDK packet and Tool Approvals Implemented#2407
sawka merged 9 commits intomainfrom
sawka/tool-approval

Conversation

@sawka
Copy link
Member

@sawka sawka commented Oct 9, 2025

provides richer information for FE to use to display tools. currently just shows pending/error/completed. but we'll use this in the future to do allow/deny flow for tools as well.

lots of backend plumbing to make it work in OpenAI. currently, not implemented for anthropic.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 9, 2025

Walkthrough

Adds structured tool-use support end-to-end: new UIMessageDataToolUse type, tool-use status/approval constants, and ToolUseData on WaveToolCall. ToolUseData is created during OpenAI streaming, carried into UI messages (parts of type "data-tooluse"), and stripped from function-call inputs before downstream calls. Implements a concurrency-safe approval registry (timeouts + keep-alive), RPC/WS handlers and a frontend RPC method, plus an AIToolUse React component with polling. SSEHandlerCh gains AiMsgData for data-* events. Telemetry TEventProps extended, SyncMap gains SetUnless/TestAndSet, and several dev-only AI UI gates removed.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title clearly describes the primary changes, namely adding a new data-tooluse AI SDK packet and implementing tool approval workflows, which directly aligns with the extensive front-end and back-end work in this pull request. It is concise, specific, and free of extraneous details, enabling a reviewer to understand the main feature at a glance. Therefore, it meets the criteria for a pass.
Description Check ✅ Passed The description outlines the addition of richer front-end display data for tool status and mentions the backend integration work for OpenAI, which directly corresponds to the changes in this pull request. It stays on topic and provides context without deviating into unrelated areas. Thus, it satisfies the lenient criteria for a pass.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch sawka/tool-approval

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f70f7fd and d4df281.

📒 Files selected for processing (1)
  • pkg/aiusechat/tools_readfile.go (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
pkg/aiusechat/tools_readfile.go (2)
pkg/util/utilfn/marshal.go (1)
  • ReUnmarshal (34-40)
pkg/aiusechat/uctypes/usechat-types.go (1)
  • ApprovalNeedsApproval (129-129)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Analyze (go)
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Build for TestDriver.ai
🔇 Additional comments (9)
pkg/aiusechat/tools_readfile.go (9)

18-20: LGTM! Well-defined constants.

The new constants ReadFileDefaultLineCount and ReadFileDefaultMaxBytes provide clear, centralized defaults that replace previous hardcoded values, improving maintainability.


59-61: Verify the negative offset validation approach.

The current implementation returns an error for negative offsets. However, a previous review comment (see past_review_comments lines 59-61) recommended normalizing negative offsets to 0 instead, to align with the prior behavior in readTextFileCallback.

If rejecting negative offsets is the intended behavior change, please confirm. Otherwise, consider applying the normalization approach as previously suggested.


30-78: LGTM! Centralized validation with proper error handling.

The parseReadTextFileInput function consolidates parameter validation and default-setting logic effectively. The function correctly handles nil input, validates required fields, and provides clear error messages.


107-110: LGTM! Refactored to use centralized validation.

The callback now uses parseReadTextFileInput for validation, eliminating duplication and ensuring consistent parameter handling across the codebase.


112-124: LGTM! Robust path handling with helpful error messages.

The addition of path expansion, stat checks, and directory detection improves robustness. The error message for directories helpfully guides users to the read_dir tool, enhancing the user experience.


126-130: LGTM! Proper file handling.

File opening uses the expanded path correctly, includes appropriate error wrapping, and ensures cleanup with defer.


146-149: LGTM! Safe pointer dereferencing.

The dereferencing of parsed parameter pointers is safe because parseReadTextFileInput ensures all these fields are non-nil with appropriate defaults.


234-254: LGTM! Well-implemented human-readable descriptions.

The ToolInputDesc callback provides clear, context-aware descriptions for different parameter combinations, enhancing the user experience when tool approval is required.


183-183: LGTM! Tool approval integration completed.

The changes correctly integrate the tool approval feature:

  • The mode field enriches the response with file permission information
  • The description accurately communicates the approval requirement
  • The ToolApproval callback properly returns ApprovalNeedsApproval

Also applies to: 196-196, 256-258


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
pkg/aiusechat/uctypes/usechat-types.go (1)

419-427: Return the slice element’s address, not the range copy.

&tool points at the loop variable copy, so any mutation on the returned pointer won’t flow back to opts.Tools/opts.TabTools. Please take the address of the slice element instead (e.g. &opts.Tools[i]) to preserve shared state.

-	for _, tool := range opts.Tools {
-		if tool.Name == toolName {
-			return &tool
-		}
-	}
+	for i := range opts.Tools {
+		if opts.Tools[i].Name == toolName {
+			return &opts.Tools[i]
+		}
+	}
-	for _, tool := range opts.TabTools {
-		if tool.Name == toolName {
-			return &tool
-		}
-	}
+	for i := range opts.TabTools {
+		if opts.TabTools[i].Name == toolName {
+			return &opts.TabTools[i]
+		}
+	}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7681214 and 1d94b55.

📒 Files selected for processing (9)
  • frontend/app/aipanel/aimessage.tsx (3 hunks)
  • frontend/app/aipanel/aitypes.ts (1 hunks)
  • pkg/aiusechat/openai/openai-backend.go (13 hunks)
  • pkg/aiusechat/openai/openai-convertmessage.go (1 hunks)
  • pkg/aiusechat/tools_screenshot.go (1 hunks)
  • pkg/aiusechat/uctypes/usechat-types.go (6 hunks)
  • pkg/aiusechat/usechat.go (3 hunks)
  • pkg/telemetry/telemetrydata/telemetrydata.go (2 hunks)
  • pkg/web/sse/ssehandler.go (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
pkg/aiusechat/usechat.go (4)
pkg/aiusechat/uctypes/usechat-types.go (8)
  • WaveChatOpts (404-416)
  • UIMessageDataToolUse (127-133)
  • WaveToolCall (149-154)
  • AIMetrics (217-233)
  • AIToolResult (271-276)
  • ToolUseStatusError (123-123)
  • ToolUseStatusCompleted (124-124)
  • WaveStopReason (156-170)
pkg/aiusechat/openai/openai-backend.go (1)
  • UpdateToolUseData (336-365)
pkg/web/sse/ssehandler.go (1)
  • SSEHandlerCh (64-77)
pkg/util/utilfn/utilfn.go (1)
  • TruncateString (151-159)
pkg/aiusechat/openai/openai-backend.go (2)
pkg/aiusechat/uctypes/usechat-types.go (7)
  • UIMessageDataToolUse (127-133)
  • WaveChatOpts (404-416)
  • AIOptsType (181-192)
  • WaveContinueResponse (173-178)
  • ToolUseStatusPending (122-122)
  • ToolUseStatusError (123-123)
  • WaveToolCall (149-154)
pkg/aiusechat/chatstore/chatstore.go (1)
  • DefaultChatStore (18-20)
pkg/aiusechat/openai/openai-convertmessage.go (1)
pkg/aiusechat/uctypes/usechat-types.go (1)
  • UIMessagePart (35-68)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Analyze (go)
  • GitHub Check: Build for TestDriver.ai
🔇 Additional comments (9)
pkg/telemetry/telemetrydata/telemetrydata.go (1)

72-121: LGTM! Well-structured telemetry additions.

The new WaveAI* telemetry fields comprehensively capture tool-use metrics (counts, errors, timing, detail maps) and align with the PR's goal of richer tool-use tracking. Field naming and JSON tagging are consistent with existing patterns.

pkg/aiusechat/usechat.go (4)

203-211: Anthropic implementation gap is acknowledged.

The function only implements tool-use data updates for OpenAI and logs a warning for Anthropic. This aligns with the PR description stating the implementation is "not currently provided for the Anthropic integration."


393-393: LGTM! Improved encapsulation.

Moving tool definition resolution to chatOpts.GetToolDefinition improves encapsulation and aligns with the new tool-definition flow introduced in this PR.


481-503: LGTM! Telemetry mapping aligns with new fields.

The telemetry event construction correctly maps all the new tool-use metrics (ToolUseCount, ToolUseErrorCount, ToolDetail, etc.) to the corresponding WaveAI* fields added in telemetrydata.go.


213-266: SSEHandlerCh.AiMsgData signature verified: method defined as AiMsgData(dataType string, id string, data interface{}) error in pkg/web/sse/ssehandler.go; matches usage.

frontend/app/aipanel/aitypes.ts (1)

13-19: LGTM! Type definition aligns with backend.

The new tooluse type structure matches the backend UIMessageDataToolUse type and uses TypeScript string literal unions for the status field, providing good type safety.

frontend/app/aipanel/aimessage.tsx (3)

95-115: LGTM! Clear and intuitive tool-use rendering.

The rendering logic for data-tooluse parts is well-structured with:

  • Intuitive status icons (✓ completed, ✗ error, • pending) and color coding
  • Proper conditional rendering of optional fields (tooldesc, errormessage)
  • Consistent styling with the existing message UI

127-133: LGTM! Maintains backward compatibility.

The updated isDisplayPart function correctly includes data-tooluse while maintaining backward compatibility with the previous tool- state-based parts.


141-147: LGTM! Content detection includes tool-use parts.

The updated hasContent check correctly treats data-tooluse parts as content, ensuring tool-use messages display appropriately instead of showing "(no text content)".

Comment on lines 812 to 841
// Create tool use data entry
toolUseData := &uctypes.UIMessageDataToolUse{
ToolCallId: st.toolCallID,
ToolName: st.toolName,
Status: uctypes.ToolUseStatusPending,
}

// Try to get tool definition
toolDef := state.chatOpts.GetToolDefinition(st.toolName)
if toolDef == nil {
toolUseData.Status = uctypes.ToolUseStatusError
toolUseData.ErrorMessage = "tool not found"
} else {
// Try to unmarshal arguments
var parsedArgs any
if err := json.Unmarshal([]byte(ev.Arguments), &parsedArgs); err != nil {
toolUseData.Status = uctypes.ToolUseStatusError
toolUseData.ErrorMessage = fmt.Sprintf("failed to parse tool arguments: %v", err)
} else {
// Get tool description if available
if toolDef.ToolInputDesc != nil {
toolUseData.ToolDesc = toolDef.ToolInputDesc(parsedArgs)
}
}
}

state.toolUseData[st.toolCallID] = toolUseData
log.Printf("AI data-tooluse %s\n", st.toolCallID)
_ = sse.AiMsgData("data-tooluse", st.toolCallID, *toolUseData)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Populate ToolDesc even when ToolInputDesc is absent.

Right now ToolDesc stays empty unless the tool defines ToolInputDesc, so most tools ship a blank description to the UI. That breaks the richer status display this PR introduces. Initialize the description with toolDef.Desc() (using your new helper) and only override it with the input-specific text when parsing succeeds.

-			toolUseData := &uctypes.UIMessageDataToolUse{
-				ToolCallId: st.toolCallID,
-				ToolName:   st.toolName,
-				Status:     uctypes.ToolUseStatusPending,
-			}
+			toolUseData := &uctypes.UIMessageDataToolUse{
+				ToolCallId: st.toolCallID,
+				ToolName:   st.toolName,
+				Status:     uctypes.ToolUseStatusPending,
+			}
@@
-			if toolDef == nil {
+			if toolDef == nil {
 				toolUseData.Status = uctypes.ToolUseStatusError
 				toolUseData.ErrorMessage = "tool not found"
 			} else {
-				// Try to unmarshal arguments
+				toolUseData.ToolDesc = toolDef.Desc()
+				// Try to unmarshal arguments
 				var parsedArgs any
 				if err := json.Unmarshal([]byte(ev.Arguments), &parsedArgs); err != nil {
 					toolUseData.Status = uctypes.ToolUseStatusError
 					toolUseData.ErrorMessage = fmt.Sprintf("failed to parse tool arguments: %v", err)
 				} else {
-					// Get tool description if available
+					// Get tool description if available
 					if toolDef.ToolInputDesc != nil {
-						toolUseData.ToolDesc = toolDef.ToolInputDesc(parsedArgs)
+						if desc := toolDef.ToolInputDesc(parsedArgs); desc != "" {
+							toolUseData.ToolDesc = desc
+						}
 					}
 				}
 			}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Create tool use data entry
toolUseData := &uctypes.UIMessageDataToolUse{
ToolCallId: st.toolCallID,
ToolName: st.toolName,
Status: uctypes.ToolUseStatusPending,
}
// Try to get tool definition
toolDef := state.chatOpts.GetToolDefinition(st.toolName)
if toolDef == nil {
toolUseData.Status = uctypes.ToolUseStatusError
toolUseData.ErrorMessage = "tool not found"
} else {
// Try to unmarshal arguments
var parsedArgs any
if err := json.Unmarshal([]byte(ev.Arguments), &parsedArgs); err != nil {
toolUseData.Status = uctypes.ToolUseStatusError
toolUseData.ErrorMessage = fmt.Sprintf("failed to parse tool arguments: %v", err)
} else {
// Get tool description if available
if toolDef.ToolInputDesc != nil {
toolUseData.ToolDesc = toolDef.ToolInputDesc(parsedArgs)
}
}
}
state.toolUseData[st.toolCallID] = toolUseData
log.Printf("AI data-tooluse %s\n", st.toolCallID)
_ = sse.AiMsgData("data-tooluse", st.toolCallID, *toolUseData)
}
// Create tool use data entry
toolUseData := &uctypes.UIMessageDataToolUse{
ToolCallId: st.toolCallID,
ToolName: st.toolName,
Status: uctypes.ToolUseStatusPending,
}
// Try to get tool definition
toolDef := state.chatOpts.GetToolDefinition(st.toolName)
if toolDef == nil {
toolUseData.Status = uctypes.ToolUseStatusError
toolUseData.ErrorMessage = "tool not found"
} else {
// Populate with default description
toolUseData.ToolDesc = toolDef.Desc()
// Try to unmarshal arguments
var parsedArgs any
if err := json.Unmarshal([]byte(ev.Arguments), &parsedArgs); err != nil {
toolUseData.Status = uctypes.ToolUseStatusError
toolUseData.ErrorMessage = fmt.Sprintf("failed to parse tool arguments: %v", err)
} else {
// Get tool description if available
if toolDef.ToolInputDesc != nil {
if desc := toolDef.ToolInputDesc(parsedArgs); desc != "" {
toolUseData.ToolDesc = desc
}
}
}
}
state.toolUseData[st.toolCallID] = toolUseData
log.Printf("AI data-tooluse %s\n", st.toolCallID)
_ = sse.AiMsgData("data-tooluse", st.toolCallID, *toolUseData)
🤖 Prompt for AI Agents
In pkg/aiusechat/openai/openai-backend.go around lines 812 to 841, ToolDesc is
only set when ToolInputDesc exists and parsing succeeds, leaving most tools with
an empty description; initialize toolUseData.ToolDesc to toolDef.Desc() (your
new helper) right after retrieving toolDef, then if ToolInputDesc is present and
argument unmarshalling succeeds, override ToolDesc with the input-specific
description; ensure this default assignment happens before parsing and error
handling so UI always receives a description.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1d94b55 and 9596cca.

📒 Files selected for processing (1)
  • pkg/aiusechat/tools_readfile.go (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
pkg/aiusechat/tools_readfile.go (1)
pkg/util/utilfn/marshal.go (1)
  • ReUnmarshal (34-40)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Analyze (go)
  • GitHub Check: Build for TestDriver.ai

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
pkg/aiusechat/tools_readfile.go (1)

27-58: Hoist the shared default line-count constant

DefaultLineCount is still defined inside this helper while readTextFileCallback redeclares it and ToolInputDesc hard-codes 100. This keeps the duplication that was called out earlier and risks the two paths diverging. Please promote the constant to package scope and reuse it everywhere (parser, callback, and descriptor).

-const StopReasonMaxBytes = "max_bytes"
+const (
+	StopReasonMaxBytes = "max_bytes"
+	DefaultLineCount   = 100
+)
-func parseReadTextFileInput(input any) (*readTextFileParams, error) {
-	const DefaultLineCount = 100
-
+func parseReadTextFileInput(input any) (*readTextFileParams, error) {
-	count := 100
+	count := DefaultLineCount
-	if origin == "start" && offset == 0 && count == 100 {
+	if origin == "start" && offset == 0 && count == DefaultLineCount {
pkg/aiusechat/openai/openai-backend.go (1)

833-848: Populate ToolDesc even without ToolInputDesc.

We still ship blank descriptions for every tool that lacks ToolInputDesc, so the new tool-use panel shows an empty body. Initialize ToolDesc from toolDef.Desc() before attempting the specialized override, and only replace it when the callback returns a non-empty string.

 func createToolUseData(toolCallID, toolName string, toolDef *uctypes.ToolDefinition, arguments string) *uctypes.UIMessageDataToolUse {
 	toolUseData := &uctypes.UIMessageDataToolUse{
 		ToolCallId: toolCallID,
 		ToolName:   toolName,
 		Status:     uctypes.ToolUseStatusPending,
 	}
 
 	if toolDef == nil {
 		toolUseData.Status = uctypes.ToolUseStatusError
 		toolUseData.ErrorMessage = "tool not found"
 		return toolUseData
 	}
 
+	toolUseData.ToolDesc = toolDef.Desc()
+
 	var parsedArgs any
 	if err := json.Unmarshal([]byte(arguments), &parsedArgs); err != nil {
 		toolUseData.Status = uctypes.ToolUseStatusError
 		toolUseData.ErrorMessage = fmt.Sprintf("failed to parse tool arguments: %v", err)
 		return toolUseData
 	}
 
 	if toolDef.ToolInputDesc != nil {
-		toolUseData.ToolDesc = toolDef.ToolInputDesc(parsedArgs)
+		if desc := toolDef.ToolInputDesc(parsedArgs); desc != "" {
+			toolUseData.ToolDesc = desc
+		}
 	}
🧹 Nitpick comments (1)
pkg/aiusechat/uctypes/usechat-types.go (1)

429-440: Return pointers to the real ToolDefinition entries.

Using for _, tool := range ... { return &tool } hands back a pointer to the loop variable copy; any later mutation (e.g., toggling approval callbacks) won’t touch the struct stored in Tools / TabTools. Iterate by index so you return the actual slice element.

-	for _, tool := range opts.Tools {
-		if tool.Name == toolName {
-			return &tool
-		}
-	}
-	for _, tool := range opts.TabTools {
-		if tool.Name == toolName {
-			return &tool
-		}
-	}
+	for i := range opts.Tools {
+		if opts.Tools[i].Name == toolName {
+			return &opts.Tools[i]
+		}
+	}
+	for i := range opts.TabTools {
+		if opts.TabTools[i].Name == toolName {
+			return &opts.TabTools[i]
+		}
+	}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9596cca and ae33947.

📒 Files selected for processing (10)
  • frontend/app/aipanel/aitypes.ts (1 hunks)
  • frontend/app/store/wshclientapi.ts (1 hunks)
  • frontend/types/gotypes.d.ts (2 hunks)
  • pkg/aiusechat/openai/openai-backend.go (13 hunks)
  • pkg/aiusechat/toolapproval.go (1 hunks)
  • pkg/aiusechat/tools_readfile.go (2 hunks)
  • pkg/aiusechat/uctypes/usechat-types.go (7 hunks)
  • pkg/wshrpc/wshclient/wshclient.go (1 hunks)
  • pkg/wshrpc/wshrpctypes.go (3 hunks)
  • pkg/wshrpc/wshserver/wshserver.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/app/aipanel/aitypes.ts
🧰 Additional context used
🧬 Code graph analysis (8)
frontend/app/store/wshclientapi.ts (1)
pkg/wshrpc/wshrpctypes.go (2)
  • CommandWaveAIToolApproveData (731-735)
  • RpcOpts (297-303)
pkg/wshrpc/wshserver/wshserver.go (4)
frontend/app/store/wshclientapi.ts (1)
  • WaveAIToolApproveCommand (496-498)
pkg/wshrpc/wshclient/wshclient.go (1)
  • WaveAIToolApproveCommand (591-594)
pkg/wshrpc/wshrpctypes.go (1)
  • CommandWaveAIToolApproveData (731-735)
pkg/aiusechat/toolapproval.go (1)
  • UpdateToolApproval (57-83)
pkg/wshrpc/wshclient/wshclient.go (3)
frontend/app/store/wshclientapi.ts (1)
  • WaveAIToolApproveCommand (496-498)
pkg/wshutil/wshrpc.go (1)
  • WshRpc (46-60)
pkg/wshrpc/wshrpctypes.go (2)
  • CommandWaveAIToolApproveData (731-735)
  • RpcOpts (297-303)
pkg/aiusechat/tools_readfile.go (2)
pkg/util/utilfn/marshal.go (1)
  • ReUnmarshal (34-40)
pkg/aiusechat/uctypes/usechat-types.go (1)
  • ApprovalNeedsApproval (129-129)
frontend/types/gotypes.d.ts (1)
pkg/wshrpc/wshrpctypes.go (1)
  • CommandWaveAIToolApproveData (731-735)
pkg/wshrpc/wshrpctypes.go (2)
frontend/app/store/wshclientapi.ts (1)
  • WaveAIToolApproveCommand (496-498)
pkg/wshrpc/wshclient/wshclient.go (1)
  • WaveAIToolApproveCommand (591-594)
pkg/aiusechat/toolapproval.go (1)
pkg/aiusechat/uctypes/usechat-types.go (2)
  • UIMessageDataToolUse (136-143)
  • ApprovalTimeout (132-132)
pkg/aiusechat/openai/openai-backend.go (2)
pkg/aiusechat/uctypes/usechat-types.go (7)
  • UIMessageDataToolUse (136-143)
  • WaveChatOpts (414-426)
  • AIOptsType (191-202)
  • ToolDefinition (78-90)
  • ToolUseStatusPending (123-123)
  • ToolUseStatusError (124-124)
  • WaveToolCall (159-164)
pkg/aiusechat/chatstore/chatstore.go (1)
  • DefaultChatStore (18-20)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Build for TestDriver.ai
  • GitHub Check: Analyze (go)
  • GitHub Check: Analyze (javascript-typescript)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
pkg/aiusechat/openai/openai-backend.go (1)

829-858: Populate ToolDesc even when ToolInputDesc is absent.

ToolDesc remains empty unless the tool defines ToolInputDesc, leaving most tools without a description in the UI. Initialize toolUseData.ToolDesc with a default from the tool definition (e.g., toolDef.Description or toolDef.ShortDescription) immediately after retrieving toolDef on line 836, then optionally override it with the input-specific description from ToolInputDesc if parsing succeeds.

Apply this diff to set a default description:

 	if toolDef == nil {
 		toolUseData.Status = uctypes.ToolUseStatusError
 		toolUseData.ErrorMessage = "tool not found"
 		return toolUseData
 	}
+	
+	// Set default description from tool definition
+	if toolDef.ShortDescription != "" {
+		toolUseData.ToolDesc = toolDef.ShortDescription
+	} else if toolDef.Description != "" {
+		toolUseData.ToolDesc = toolDef.Description
+	}
 
 	var parsedArgs any
 	if err := json.Unmarshal([]byte(arguments), &parsedArgs); err != nil {
 		toolUseData.Status = uctypes.ToolUseStatusError
 		toolUseData.ErrorMessage = fmt.Sprintf("failed to parse tool arguments: %v", err)
 		return toolUseData
 	}
 
 	if toolDef.ToolInputDesc != nil {
-		toolUseData.ToolDesc = toolDef.ToolInputDesc(parsedArgs)
+		if desc := toolDef.ToolInputDesc(parsedArgs); desc != "" {
+			toolUseData.ToolDesc = desc
+		}
 	}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae33947 and 0b3b00a.

📒 Files selected for processing (8)
  • frontend/app/aipanel/aimessage.tsx (5 hunks)
  • pkg/aiusechat/openai/openai-backend.go (13 hunks)
  • pkg/aiusechat/toolapproval.go (1 hunks)
  • pkg/aiusechat/tools.go (1 hunks)
  • pkg/aiusechat/tools_readfile.go (5 hunks)
  • pkg/aiusechat/uctypes/usechat-types.go (7 hunks)
  • pkg/aiusechat/usechat.go (9 hunks)
  • pkg/util/ds/syncmap.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/aiusechat/tools_readfile.go
🧰 Additional context used
🧬 Code graph analysis (6)
pkg/aiusechat/tools.go (1)
pkg/aiusechat/tools_readfile.go (1)
  • GetReadTextFileToolDefinition (207-284)
pkg/aiusechat/toolapproval.go (1)
pkg/aiusechat/uctypes/usechat-types.go (1)
  • ApprovalTimeout (132-132)
frontend/app/aipanel/aimessage.tsx (3)
frontend/app/aipanel/aitypes.ts (1)
  • WaveUIMessagePart (24-24)
frontend/app/store/wshclientapi.ts (1)
  • RpcApi (537-537)
frontend/app/store/wshrpcutil.ts (1)
  • TabRpcClient (37-37)
pkg/aiusechat/openai/openai-backend.go (3)
pkg/aiusechat/uctypes/usechat-types.go (8)
  • UIMessageDataToolUse (136-143)
  • WaveChatOpts (418-431)
  • AIOptsType (195-206)
  • ApprovalNeedsApproval (129-129)
  • ToolDefinition (78-90)
  • ToolUseStatusPending (123-123)
  • ToolUseStatusError (124-124)
  • WaveToolCall (163-168)
pkg/aiusechat/chatstore/chatstore.go (1)
  • DefaultChatStore (18-20)
pkg/aiusechat/toolapproval.go (1)
  • RegisterToolApproval (48-58)
pkg/aiusechat/usechat.go (7)
pkg/util/ds/syncmap.go (1)
  • MakeSyncMap (13-18)
pkg/aiusechat/uctypes/usechat-types.go (11)
  • WaveChatOpts (418-431)
  • UIMessageDataToolUse (136-143)
  • WaveToolCall (163-168)
  • AIMetrics (231-247)
  • AIToolResult (285-290)
  • ToolUseStatusError (124-124)
  • ApprovalNeedsApproval (129-129)
  • ApprovalUserDenied (131-131)
  • ApprovalTimeout (132-132)
  • ToolUseStatusCompleted (125-125)
  • WaveStopReason (170-184)
pkg/aiusechat/openai/openai-backend.go (1)
  • UpdateToolUseData (336-365)
pkg/web/sse/ssehandler.go (1)
  • SSEHandlerCh (64-77)
pkg/util/utilfn/utilfn.go (1)
  • TruncateString (151-159)
pkg/aiusechat/toolapproval.go (2)
  • WaitForToolApproval (99-116)
  • RegisterToolApproval (48-58)
pkg/aiusechat/openai/openai-convertmessage.go (1)
  • ConvertToolResultsToOpenAIChatMessage (362-416)
pkg/aiusechat/uctypes/usechat-types.go (1)
pkg/aiusechat/toolapproval.go (1)
  • RegisterToolApproval (48-58)
🪛 Biome (2.1.2)
frontend/app/aipanel/aimessage.tsx

[error] 92-92: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

Hooks should not be called after an early return.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Analyze (go)
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Build for TestDriver.ai
🔇 Additional comments (13)
pkg/aiusechat/usechat.go (7)

24-24: LGTM!

The SyncMap initialization provides thread-safe tracking of active tools and chats, preventing race conditions during concurrent AI operations.

Also applies to: 45-46


207-215: LGTM!

The helper correctly updates tool use data for OpenAI. The warning for Anthropic is expected per PR objectives (Anthropic implementation deferred).


217-304: LGTM!

The tool call processing implementation is comprehensive:

  • Validates ToolUseData presence
  • Handles error status correctly
  • Implements approval flow with proper status transitions
  • Emits SSE updates at each state change
  • Updates metrics including error counts
  • Logs truncated input for debugging without exposing full data

306-335: LGTM!

The orchestration correctly:

  • Marks all tool calls as active with deferred cleanup (lines 308-310)
  • Processes each tool call sequentially
  • Converts results to API-specific message format
  • Posts messages to chat store

Note: Conversion errors on lines 321, 330 are logged but not propagated, which is appropriate since tool execution errors are already captured in the result objects.


339-342: LGTM!

The concurrency guard correctly prevents multiple simultaneous runs of the same chat using atomic test-and-set semantics, with guaranteed cleanup via defer.


407-407: LGTM!

The integration of processToolCalls and the telemetry updates are correct. All relevant metrics including the new WaveAIToolUseErrorCount are properly captured and sent.

Also applies to: 529-547


600-604: LGTM!

The RegisterToolApproval callback is correctly wired into WaveChatOpts, enabling the approval flow for tool usage.

pkg/aiusechat/openai/openai-backend.go (6)

42-49: LGTM!

The ToolUseData field and Clean() method correctly implement the pattern of attaching UI-specific metadata that must be stripped before sending to the external API. The method safely creates a copy without modifying the original.

Also applies to: 97-104


326-331: LGTM!

The streaming state additions correctly track per-call tool use data and propagate chat options needed for tool definition lookups during streaming.


336-365: LGTM!

The UpdateToolUseData function correctly:

  • Validates chat existence
  • Searches for the target function call by CallId
  • Creates new copies to avoid mutation
  • Posts the updated message to the chat store
  • Returns appropriate errors when chat or call is not found

423-424: LGTM!

The Clean() call correctly strips UI-only ToolUseData before sending function calls to the external API.


812-820: LGTM!

The tool use data emission flow correctly:

  • Retrieves tool definition
  • Creates structured tool use data
  • Stores it in streaming state for later attachment
  • Registers approval request when needed
  • Emits to frontend via SSE

883-900: LGTM!

The ToolUseData attachment logic correctly:

  • Looks up tool use data by CallId from the streaming state
  • Attaches it to WaveToolCall for the stop reason
  • Attaches it to OpenAIFunctionCallInput for message storage
  • Logs when data is unexpectedly missing for debugging

This ensures the UI-specific tool status information is preserved throughout the message flow.

Also applies to: 909-920

Comment on lines +76 to +146
const AIToolUse = memo(({ part }: AIToolUseProps) => {
const toolData = part.data;
const [userApprovalOverride, setUserApprovalOverride] = useState<string | null>(null);

if (!toolData) return null;

const statusIcon = toolData.status === "completed" ? "✓" : toolData.status === "error" ? "✗" : "•";
const statusColor =
toolData.status === "completed"
? "text-green-400"
: toolData.status === "error"
? "text-red-400"
: "text-gray-400";

const effectiveApproval = userApprovalOverride || toolData.approval;

useEffect(() => {
if (effectiveApproval !== "needs-approval") return;

const interval = setInterval(() => {
RpcApi.WaveAIToolApproveCommand(TabRpcClient, {
toolcallid: toolData.toolcallid,
keepalive: true,
});
}, 4000);

return () => clearInterval(interval);
}, [effectiveApproval, toolData.toolcallid]);

const handleApprove = () => {
setUserApprovalOverride("user-approved");
RpcApi.WaveAIToolApproveCommand(TabRpcClient, {
toolcallid: toolData.toolcallid,
approval: "user-approved",
});
};

const handleDeny = () => {
setUserApprovalOverride("user-denied");
RpcApi.WaveAIToolApproveCommand(TabRpcClient, {
toolcallid: toolData.toolcallid,
approval: "user-denied",
});
};

return (
<div className={cn("flex items-start gap-2 p-2 rounded bg-gray-800 border border-gray-700", statusColor)}>
<span className="font-bold">{statusIcon}</span>
<div className="flex-1">
<div className="font-semibold">{toolData.toolname}</div>
{toolData.tooldesc && <div className="text-sm text-gray-400">{toolData.tooldesc}</div>}
{toolData.errormessage && <div className="text-sm text-red-300 mt-1">{toolData.errormessage}</div>}
{effectiveApproval === "needs-approval" && (
<div className="mt-2 flex gap-2">
<button
onClick={handleApprove}
className="px-3 py-1 border border-gray-600 text-gray-300 hover:border-gray-500 hover:text-white text-sm rounded cursor-pointer transition-colors"
>
Approve
</button>
<button
onClick={handleDeny}
className="px-3 py-1 border border-gray-600 text-gray-300 hover:border-gray-500 hover:text-white text-sm rounded cursor-pointer transition-colors"
>
Deny
</button>
</div>
)}
</div>
</div>
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix hook ordering and clear stale approval overrides

useEffect sits behind the if (!toolData) return null guard, so the hook is skipped on the initial render whenever the packet arrives without data (common mid-stream). React then sees a different hook order when toolData becomes available and throws. Lint already flags this. In addition, userApprovalOverride never resets when a new tool call replaces the part or when the backend reports a different approval; the stale override keeps future requests stuck in the old state so the UI can show “denied” for a brand-new tool and stop emitting keep-alives.

Restructure the component so hooks run unconditionally, gate the keep-alive effect internally, and reset the override when the toolcall or backend approval changes:

 const AIToolUse = memo(({ part }: AIToolUseProps) => {
-    const toolData = part.data;
-    const [userApprovalOverride, setUserApprovalOverride] = useState<string | null>(null);
-
-    if (!toolData) return null;
-
-    const statusIcon = toolData.status === "completed" ? "✓" : toolData.status === "error" ? "✗" : "•";
-    const statusColor =
-        toolData.status === "completed"
-            ? "text-green-400"
-            : toolData.status === "error"
-              ? "text-red-400"
-              : "text-gray-400";
-
-    const effectiveApproval = userApprovalOverride || toolData.approval;
-
-    useEffect(() => {
-        if (effectiveApproval !== "needs-approval") return;
-
-        const interval = setInterval(() => {
-            RpcApi.WaveAIToolApproveCommand(TabRpcClient, {
-                toolcallid: toolData.toolcallid,
-                keepalive: true,
-            });
-        }, 4000);
-
-        return () => clearInterval(interval);
-    }, [effectiveApproval, toolData.toolcallid]);
+    const toolData = part.data;
+    const [userApprovalOverride, setUserApprovalOverride] = useState<string | null>(null);
+    const toolCallId = toolData?.toolcallid;
+    const effectiveApproval = userApprovalOverride ?? toolData?.approval ?? "";
+
+    useEffect(() => {
+        if (!toolCallId || effectiveApproval !== "needs-approval") {
+            return;
+        }
+        const interval = setInterval(() => {
+            RpcApi.WaveAIToolApproveCommand(TabRpcClient, {
+                toolcallid: toolCallId,
+                keepalive: true,
+            });
+        }, 4000);
+        return () => clearInterval(interval);
+    }, [effectiveApproval, toolCallId]);
+
+    useEffect(() => {
+        setUserApprovalOverride(null);
+    }, [toolCallId, toolData?.approval]);
+
+    if (!toolData) {
+        return null;
+    }
+
+    const statusIcon = toolData.status === "completed" ? "✓" : toolData.status === "error" ? "✗" : "•";
+    const statusColor =
+        toolData.status === "completed"
+            ? "text-green-400"
+            : toolData.status === "error"
+              ? "text-red-400"
+              : "text-gray-400";

With this shape the hook order stays fixed and the UI no longer carries approval decisions across different tool calls.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const AIToolUse = memo(({ part }: AIToolUseProps) => {
const toolData = part.data;
const [userApprovalOverride, setUserApprovalOverride] = useState<string | null>(null);
if (!toolData) return null;
const statusIcon = toolData.status === "completed" ? "✓" : toolData.status === "error" ? "✗" : "•";
const statusColor =
toolData.status === "completed"
? "text-green-400"
: toolData.status === "error"
? "text-red-400"
: "text-gray-400";
const effectiveApproval = userApprovalOverride || toolData.approval;
useEffect(() => {
if (effectiveApproval !== "needs-approval") return;
const interval = setInterval(() => {
RpcApi.WaveAIToolApproveCommand(TabRpcClient, {
toolcallid: toolData.toolcallid,
keepalive: true,
});
}, 4000);
return () => clearInterval(interval);
}, [effectiveApproval, toolData.toolcallid]);
const handleApprove = () => {
setUserApprovalOverride("user-approved");
RpcApi.WaveAIToolApproveCommand(TabRpcClient, {
toolcallid: toolData.toolcallid,
approval: "user-approved",
});
};
const handleDeny = () => {
setUserApprovalOverride("user-denied");
RpcApi.WaveAIToolApproveCommand(TabRpcClient, {
toolcallid: toolData.toolcallid,
approval: "user-denied",
});
};
return (
<div className={cn("flex items-start gap-2 p-2 rounded bg-gray-800 border border-gray-700", statusColor)}>
<span className="font-bold">{statusIcon}</span>
<div className="flex-1">
<div className="font-semibold">{toolData.toolname}</div>
{toolData.tooldesc && <div className="text-sm text-gray-400">{toolData.tooldesc}</div>}
{toolData.errormessage && <div className="text-sm text-red-300 mt-1">{toolData.errormessage}</div>}
{effectiveApproval === "needs-approval" && (
<div className="mt-2 flex gap-2">
<button
onClick={handleApprove}
className="px-3 py-1 border border-gray-600 text-gray-300 hover:border-gray-500 hover:text-white text-sm rounded cursor-pointer transition-colors"
>
Approve
</button>
<button
onClick={handleDeny}
className="px-3 py-1 border border-gray-600 text-gray-300 hover:border-gray-500 hover:text-white text-sm rounded cursor-pointer transition-colors"
>
Deny
</button>
</div>
)}
</div>
</div>
);
const AIToolUse = memo(({ part }: AIToolUseProps) => {
const toolData = part.data;
const [userApprovalOverride, setUserApprovalOverride] = useState<string | null>(null);
const toolCallId = toolData?.toolcallid;
const effectiveApproval = userApprovalOverride ?? toolData?.approval ?? "";
useEffect(() => {
if (!toolCallId || effectiveApproval !== "needs-approval") {
return;
}
const interval = setInterval(() => {
RpcApi.WaveAIToolApproveCommand(TabRpcClient, {
toolcallid: toolCallId,
keepalive: true,
});
}, 4000);
return () => clearInterval(interval);
}, [effectiveApproval, toolCallId]);
useEffect(() => {
setUserApprovalOverride(null);
}, [toolCallId, toolData?.approval]);
if (!toolData) {
return null;
}
const statusIcon = toolData.status === "completed" ? "✓" : toolData.status === "error" ? "✗" : "•";
const statusColor =
toolData.status === "completed"
? "text-green-400"
: toolData.status === "error"
? "text-red-400"
: "text-gray-400";
// …rest of component (handlers, JSX) remains unchanged
});
🧰 Tools
🪛 Biome (2.1.2)

[error] 92-92: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

Hooks should not be called after an early return.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

🤖 Prompt for AI Agents
In frontend/app/aipanel/aimessage.tsx around lines 76-146, the component
conditionally returns before running hooks which breaks hook ordering and the
userApprovalOverride can become stale across tool calls; move all hooks
(useState and useEffect) to run unconditionally (above the early return), change
the keep-alive effect to internally no-op unless toolData exists and
effectiveApproval === "needs-approval", and add a useEffect that resets
userApprovalOverride to null whenever toolData?.toolcallid or toolData?.approval
changes; also update effect dependency arrays to include toolData.toolcallid and
toolData.approval so keep-alives and resets run correctly.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/aiusechat/tools_readfile.go (1)

129-135: Add missing min helper
Define func min(a, b int) int { if a < b { return a } return b } in this package or inline the comparison to avoid the undefined min symbol.

♻️ Duplicate comments (3)
pkg/aiusechat/tools_readfile.go (2)

28-61: Consolidate defaults and complete validation in parser.

Avoid duplicating DefaultLineCount and add missing checks (origin, count>=1, normalize negative offset). Centralize default for consistency.

- func parseReadTextFileInput(input any) (*readTextFileParams, error) {
-   const DefaultLineCount = 100
+ const DefaultLineCount = 100
+
+ func parseReadTextFileInput(input any) (*readTextFileParams, error) {
    result := &readTextFileParams{}
@@
-  if result.Count == nil {
-    count := DefaultLineCount
-    result.Count = &count
-  }
-
-  return result, nil
+  if result.Count == nil {
+    count := DefaultLineCount
+    result.Count = &count
+  }
+  if *result.Count < 1 {
+    return nil, fmt.Errorf("count must be at least 1, got %d", *result.Count)
+  }
+  if *result.Origin != "start" && *result.Origin != "end" {
+    return nil, fmt.Errorf("invalid origin value %q: must be 'start' or 'end'", *result.Origin)
+  }
+  if *result.Offset < 0 {
+    zero := 0
+    result.Offset = &zero
+  }
+  return result, nil

From past review comments


249-278: Use DefaultLineCount in ToolInputDesc (avoid hardcoded 100).

-  count := 100
+  count := DefaultLineCount
   if parsed.Count != nil {
     count = *parsed.Count
   }

From past review comments

pkg/aiusechat/openai/openai-backend.go (1)

829-858: Initialize ToolDesc with default value before checking ToolInputDesc.

Currently, ToolDesc remains empty unless ToolInputDesc is defined (lines 849-851). Tools without ToolInputDesc will ship a blank description to the UI, undermining the richer status display this PR introduces. Initialize ToolDesc with toolDef.Desc() immediately after the nil check (line 836), then override it if ToolInputDesc provides a more specific description.

Apply this diff to provide a default description:

 func createToolUseData(toolCallID, toolName string, toolDef *uctypes.ToolDefinition, arguments string) *uctypes.UIMessageDataToolUse {
 	toolUseData := &uctypes.UIMessageDataToolUse{
 		ToolCallId: toolCallID,
 		ToolName:   toolName,
 		Status:     uctypes.ToolUseStatusPending,
 	}
 
 	if toolDef == nil {
 		toolUseData.Status = uctypes.ToolUseStatusError
 		toolUseData.ErrorMessage = "tool not found"
 		return toolUseData
 	}
 
+	toolUseData.ToolDesc = toolDef.Desc()
+
 	var parsedArgs any
 	if err := json.Unmarshal([]byte(arguments), &parsedArgs); err != nil {
 		toolUseData.Status = uctypes.ToolUseStatusError
 		toolUseData.ErrorMessage = fmt.Sprintf("failed to parse tool arguments: %v", err)
 		return toolUseData
 	}
 
 	if toolDef.ToolInputDesc != nil {
-		toolUseData.ToolDesc = toolDef.ToolInputDesc(parsedArgs)
+		if desc := toolDef.ToolInputDesc(parsedArgs); desc != "" {
+			toolUseData.ToolDesc = desc
+		}
 	}
 
 	if toolDef.ToolApproval != nil {
 		toolUseData.Approval = toolDef.ToolApproval(parsedArgs)
 	}
 
 	return toolUseData
 }
🧹 Nitpick comments (2)
pkg/util/ds/syncmap.go (1)

55-64: Document that testFn executes under the map lock.

testFn runs while holding the mutex. If it blocks or re-enters SyncMap, it can deadlock. Add a doc comment advising testFn be fast/non‑blocking and not call back into SyncMap.

pkg/aiusechat/usechat.go (1)

306-316: Minor: avoid defers inside loop for cleanup (optional).

Deferring Delete per iteration is fine for small N; alternatively collect IDs and delete in one deferred function to reduce defer overhead.

- for _, toolCall := range stopReason.ToolCalls {
-   activeToolMap.Set(toolCall.ID, true)
-   defer activeToolMap.Delete(toolCall.ID)
- }
+ var ids []string
+ for _, c := range stopReason.ToolCalls {
+   activeToolMap.Set(c.ID, true)
+   ids = append(ids, c.ID)
+ }
+ defer func() {
+   for _, id := range ids {
+     activeToolMap.Delete(id)
+   }
+ }()
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae33947 and 1a28f77.

📒 Files selected for processing (9)
  • frontend/app/aipanel/aimessage.tsx (5 hunks)
  • frontend/app/workspace/workspace-layout-model.ts (2 hunks)
  • pkg/aiusechat/openai/openai-backend.go (13 hunks)
  • pkg/aiusechat/toolapproval.go (1 hunks)
  • pkg/aiusechat/tools.go (1 hunks)
  • pkg/aiusechat/tools_readfile.go (5 hunks)
  • pkg/aiusechat/uctypes/usechat-types.go (7 hunks)
  • pkg/aiusechat/usechat.go (9 hunks)
  • pkg/util/ds/syncmap.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (7)
pkg/aiusechat/usechat.go (7)
pkg/util/ds/syncmap.go (1)
  • MakeSyncMap (13-18)
pkg/aiusechat/uctypes/usechat-types.go (11)
  • WaveChatOpts (418-431)
  • UIMessageDataToolUse (136-143)
  • WaveToolCall (163-168)
  • AIMetrics (231-247)
  • AIToolResult (285-290)
  • ToolUseStatusError (124-124)
  • ApprovalNeedsApproval (129-129)
  • ApprovalUserDenied (131-131)
  • ApprovalTimeout (132-132)
  • ToolUseStatusCompleted (125-125)
  • WaveStopReason (170-184)
pkg/aiusechat/openai/openai-backend.go (1)
  • UpdateToolUseData (336-365)
pkg/web/sse/ssehandler.go (1)
  • SSEHandlerCh (64-77)
pkg/util/utilfn/utilfn.go (1)
  • TruncateString (151-159)
pkg/aiusechat/toolapproval.go (2)
  • WaitForToolApproval (99-116)
  • RegisterToolApproval (48-58)
pkg/aiusechat/openai/openai-convertmessage.go (1)
  • ConvertToolResultsToOpenAIChatMessage (362-416)
pkg/aiusechat/tools.go (1)
pkg/aiusechat/tools_readfile.go (1)
  • GetReadTextFileToolDefinition (207-284)
pkg/aiusechat/toolapproval.go (1)
pkg/aiusechat/uctypes/usechat-types.go (1)
  • ApprovalTimeout (132-132)
pkg/aiusechat/tools_readfile.go (2)
pkg/util/utilfn/marshal.go (1)
  • ReUnmarshal (34-40)
pkg/aiusechat/uctypes/usechat-types.go (1)
  • ApprovalNeedsApproval (129-129)
pkg/aiusechat/openai/openai-backend.go (3)
pkg/aiusechat/uctypes/usechat-types.go (8)
  • UIMessageDataToolUse (136-143)
  • WaveChatOpts (418-431)
  • AIOptsType (195-206)
  • ApprovalNeedsApproval (129-129)
  • ToolDefinition (78-90)
  • ToolUseStatusPending (123-123)
  • ToolUseStatusError (124-124)
  • WaveToolCall (163-168)
pkg/aiusechat/chatstore/chatstore.go (1)
  • DefaultChatStore (18-20)
pkg/aiusechat/toolapproval.go (1)
  • RegisterToolApproval (48-58)
pkg/aiusechat/uctypes/usechat-types.go (1)
pkg/aiusechat/toolapproval.go (1)
  • RegisterToolApproval (48-58)
frontend/app/aipanel/aimessage.tsx (3)
frontend/app/aipanel/aitypes.ts (1)
  • WaveUIMessagePart (24-24)
frontend/app/store/wshclientapi.ts (1)
  • RpcApi (537-537)
frontend/app/store/wshrpcutil.ts (1)
  • TabRpcClient (37-37)
🪛 Biome (2.1.2)
frontend/app/aipanel/aimessage.tsx

[error] 92-92: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

Hooks should not be called after an early return.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build for TestDriver.ai
  • GitHub Check: Analyze (go)
🔇 Additional comments (14)
frontend/app/workspace/workspace-layout-model.ts (1)

10-10: LGTM: Clean import removal.

The removal of the isDev import is correct and aligns with the removal of development-mode gating throughout this file.

pkg/aiusechat/tools.go (1)

128-130: Adding read_text_file tool is correct and well‑scoped behind widgetAccess.

Tool includes approval gating; pairs well with screenshot tool. LGTM.

pkg/aiusechat/tools_readfile.go (1)

106-124: Path handling and directory guard look good.

Home expansion, stat error paths, and directory rejection are correct and user-friendly.

pkg/aiusechat/usechat.go (4)

207-215: OpenAI chat update path is fine; Anthropic warn is acceptable placeholder.

Clean separation; logs on failure avoid hard fail. LGTM.


217-304: Tool-use lifecycle handling (error/approval/completion) is sound.

  • Validates ToolUseData, logs input safely, updates SSE and chat.
  • Approval wait path sets error/status on deny/timeout.
    Good structure.

337-343: Good guard against concurrent RunAIChat per chat.

Atomic SetUnless prevents overlapping runs; deferred delete ensures cleanup. LGTM.


527-547: Telemetry fields extended appropriately.

WaveAI fields cover tool-use counts and details. LGTM.

pkg/aiusechat/toolapproval.go (1)

60-116: LGTM! Deadlock issue from previous review has been resolved.

The approval workflow implementation is now correct. The previous deadlock concern has been addressed by deferring cleanup to WaitForToolApproval (lines 111-113) rather than attempting to unregister within UpdateToolApproval while holding the per-request lock. The timer callback at line 54 can now safely invoke UpdateToolApproval without blocking.

pkg/aiusechat/uctypes/usechat-types.go (4)

89-89: LGTM!

The ToolApproval callback enables tools to declare their approval requirements based on input arguments.


102-110: LGTM!

The Desc() helper appropriately prefers ShortDescription and includes a nil-receiver check.


122-134: LGTM!

The status and approval constants provide clear states for the tool-use lifecycle.


136-147: Empty Approval string treated as approved by design. All tools lacking a ToolApproval callback default to no-approval-required (empty Approval passes IsApproved()); only read_text_file requests explicit approval.

pkg/aiusechat/openai/openai-backend.go (2)

97-104: LGTM!

The Clean() method properly strips internal ToolUseData before sending to external APIs.


336-365: LGTM!

UpdateToolUseData correctly creates copies of the message and function call structures before updating, preventing unintended mutations of the original chat history.

Comment on lines +76 to +104
const AIToolUse = memo(({ part }: AIToolUseProps) => {
const toolData = part.data;
const [userApprovalOverride, setUserApprovalOverride] = useState<string | null>(null);

if (!toolData) return null;

const statusIcon = toolData.status === "completed" ? "✓" : toolData.status === "error" ? "✗" : "•";
const statusColor =
toolData.status === "completed"
? "text-green-400"
: toolData.status === "error"
? "text-red-400"
: "text-gray-400";

const effectiveApproval = userApprovalOverride || toolData.approval;

useEffect(() => {
if (effectiveApproval !== "needs-approval") return;

const interval = setInterval(() => {
RpcApi.WaveAIToolApproveCommand(TabRpcClient, {
toolcallid: toolData.toolcallid,
keepalive: true,
});
}, 4000);

return () => clearInterval(interval);
}, [effectiveApproval, toolData.toolcallid]);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix conditional hook and unsafe dependency (can crash when part.data is null).

useEffect is after an early return and reads toolData.toolcallid unguarded. Call hooks unconditionally and guard nulls.

Apply this diff:

 const AIToolUse = memo(({ part }: AIToolUseProps) => {
   const toolData = part.data;
   const [userApprovalOverride, setUserApprovalOverride] = useState<string | null>(null);
-  
-  if (!toolData) return null;
+  const callId = toolData?.toolcallid;
+
+  // Keep-alive while awaiting approval
+  useEffect(() => {
+    if (effectiveApproval !== "needs-approval" || !callId) return;
+    const interval = setInterval(() => {
+      RpcApi.WaveAIToolApproveCommand(TabRpcClient, {
+        toolcallid: callId,
+        keepalive: true,
+      });
+    }, 4000);
+    return () => clearInterval(interval);
+  }, [effectiveApproval, callId]);
 
-  const statusIcon = toolData.status === "completed" ? "✓" : toolData.status === "error" ? "✗" : "•";
+  if (!toolData) return null;
+  const statusIcon = toolData.status === "completed" ? "✓" : toolData.status === "error" ? "✗" : "•";
   const statusColor =
     toolData.status === "completed"
       ? "text-green-400"
       : toolData.status === "error"
         ? "text-red-400"
         : "text-gray-400";
 
   const effectiveApproval = userApprovalOverride || toolData.approval;
-
-  useEffect(() => {
-    if (effectiveApproval !== "needs-approval") return;
-
-    const interval = setInterval(() => {
-      RpcApi.WaveAIToolApproveCommand(TabRpcClient, {
-        toolcallid: toolData.toolcallid,
-        keepalive: true,
-      });
-    }, 4000);
-
-    return () => clearInterval(interval);
-  }, [effectiveApproval, toolData.toolcallid]);

As per static analysis hints

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Biome (2.1.2)

[error] 92-92: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

Hooks should not be called after an early return.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

🤖 Prompt for AI Agents
frontend/app/aipanel/aimessage.tsx around lines 76 to 104: the component
currently returns early before calling hooks (creating a conditional hook) and
the useEffect reads toolData.toolcallid unguarded which can crash when part.data
is null; fix by declaring hooks (useState, useEffect) unconditionally before any
early returns, compute effectiveApproval using userApprovalOverride ??
toolData?.approval, and in the effect guard for toolData existence (if
(!toolData || effectiveApproval !== "needs-approval") return;), use optional
chaining for the toolcallid when calling the RPC and put toolData?.toolcallid in
the dependency array so the effect won’t access a null property.

this.aiPanelWrapperRef = null;
this.inResize = false;
this.aiPanelVisible = isDev();
this.aiPanelVisible = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Verify the AI panel is now generally available and ready for production.

The AI panel now defaults to visible for all users (previously gated by isDev()). This is a significant UX change that exposes the new tool-use features to production.

Please confirm:

  1. This change is intentional and aligns with the product roadmap for making AI features generally available.
  2. All isDev() checks related to AI panel functionality have been removed consistently across the codebase (frontend components, handlers, etc.).
  3. The AI panel and tool-use features are fully functional and tested in production environments.
  4. Existing users who may have had implicit "closed" state won't experience issues (the code shows visibility is persisted in tab metadata at lines 76-82, which should handle this).

Run the following script to verify that all isDev() guards related to AI panel functionality have been removed:


🏁 Script executed:

#!/bin/bash
# Description: Search for remaining isDev() checks related to AI panel or WaveAI functionality.

# Search for isDev usage in frontend AI-related files
echo "=== Searching for isDev() in AI-related frontend files ==="
rg -n --type=ts -C3 'isDev\s*\(' frontend/app/aipanel/ frontend/app/workspace/

# Search for isDev usage specifically around aiPanel or waveai keywords
echo -e "\n=== Searching for isDev() near AI panel keywords ==="
rg -nP --type=ts -C5 'isDev\s*\(' | rg -C5 '(?i)(aipanel|waveai|ai.*panel)'

# Search for any commented-out isDev checks that might indicate incomplete removal
echo -e "\n=== Searching for commented isDev() references ==="
rg -n --type=ts '//.*isDev' frontend/app/aipanel/ frontend/app/workspace/

Length of output: 2992


Remove leftover isDev() guards on AI features

The following dev‐only conditionals remain and must be removed so AI features are always available:

  • frontend/app/workspace/widgets.tsx (Wave Dev Build label at lines ~222–225 & 246–249)
  • frontend/app/tab/tabbar.tsx (waveaiButton at lines ~643–648)

Remove these isDev() checks to ensure consistent production visibility.

Comment on lines +433 to +445
func (opts *WaveChatOpts) GetToolDefinition(toolName string) *ToolDefinition {
for _, tool := range opts.Tools {
if tool.Name == toolName {
return &tool
}
}
for _, tool := range opts.TabTools {
if tool.Name == toolName {
return &tool
}
}
return nil
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Fix pointer-to-loop-variable bug in GetToolDefinition.

Returning &tool at lines 436 and 441 creates a pointer to the loop variable, which is reused across iterations. All returned pointers reference the same memory location (the last iterated tool), causing incorrect tool definitions to be used at runtime.

Apply this diff to fix the bug by indexing into the slice:

 func (opts *WaveChatOpts) GetToolDefinition(toolName string) *ToolDefinition {
-	for _, tool := range opts.Tools {
-		if tool.Name == toolName {
-			return &tool
+	for i := range opts.Tools {
+		if opts.Tools[i].Name == toolName {
+			return &opts.Tools[i]
 		}
 	}
-	for _, tool := range opts.TabTools {
-		if tool.Name == toolName {
-			return &tool
+	for i := range opts.TabTools {
+		if opts.TabTools[i].Name == toolName {
+			return &opts.TabTools[i]
 		}
 	}
 	return nil
 }
🤖 Prompt for AI Agents
In pkg/aiusechat/uctypes/usechat-types.go around lines 433 to 445, the function
GetToolDefinition returns &tool which is a pointer to the loop variable (reused
each iteration) and causes all returned pointers to point to the same memory;
change the loops to use index-based access and return pointers to the actual
slice elements (e.g. return &opts.Tools[i] and &opts.TabTools[j]) or otherwise
take the address of the slice element rather than the loop variable so each
returned pointer references the correct ToolDefinition.

@sawka sawka changed the title new data-tooluse AI SDK packet for frontend New data-tooluse AI SDK packet and Tool Approvals Implemented Oct 9, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (4)
pkg/aiusechat/tools_readfile.go (3)

28-61: Add missing validation to complete the function.

The previous critical issue with the double pointer (line 37) has been fixed. However, the validation gaps identified in the earlier review remain:

  1. Missing origin validation (compare to lines 145-147): No check that origin is "start" or "end"
  2. Missing count validation (compare to lines 157-159): No check that count >= 1
  3. Missing offset normalization (compare to lines 162-164): Negative offsets should be normalized to 0

Without these checks, parseReadTextFileInput cannot serve as a complete validation function, limiting its reusability and creating inconsistency with readTextFileCallback.

Apply this diff to add the missing validation:

 	if result.Count == nil {
 		count := DefaultLineCount
 		result.Count = &count
 	}
+	if *result.Count < 1 {
+		return nil, fmt.Errorf("count must be at least 1, got %d", *result.Count)
+	}
+
+	if *result.Origin != "start" && *result.Origin != "end" {
+		return nil, fmt.Errorf("invalid origin value '%s': must be 'start' or 'end'", *result.Origin)
+	}
+
+	if *result.Offset < 0 {
+		offset := 0
+		result.Offset = &offset
+	}
 
 	return result, nil
 }

29-29: Extract duplicate constant to module level.

DefaultLineCount is defined in both parseReadTextFileInput (line 29) and readTextFileCallback (line 88). This duplication creates a maintenance risk.

Apply this diff to define the constant once at module level:

 const StopReasonMaxBytes = "max_bytes"
+const DefaultLineCount = 100
 
 type readTextFileParams struct {

Then remove the local definitions:

 func parseReadTextFileInput(input any) (*readTextFileParams, error) {
-	const DefaultLineCount = 100
-
 	result := &readTextFileParams{}
 func readTextFileCallback(input any) (any, error) {
-	const DefaultLineCount = 100
 	const DefaultMaxBytes = 50 * 1024
 	const ReadLimit = 1024 * 1024 * 1024

Also applies to: 88-88


249-278: Replace hardcoded value with constant.

The ToolInputDesc implementation provides helpful human-readable descriptions. However, line 263 uses the hardcoded value 100 instead of referencing DefaultLineCount, creating a maintenance issue if the default changes.

Apply this diff after extracting DefaultLineCount to module level:

-			count := 100
+			count := DefaultLineCount
 			if parsed.Count != nil {
 				count = *parsed.Count
 			}
 
-			if origin == "start" && offset == 0 {
+			if origin == "start" && offset == 0 && count == DefaultLineCount {
 				return fmt.Sprintf("reading %q (first %d lines)", parsed.Filename, count)
 			}
-			if origin == "end" && offset == 0 {
+			if origin == "end" && offset == 0 && count == DefaultLineCount {
 				return fmt.Sprintf("reading %q (last %d lines)", parsed.Filename, count)
 			}
frontend/app/aipanel/aimessage.tsx (1)

76-101: Fix null safety and add state reset for tool call changes.

The component has two critical issues:

  1. toolData properties are accessed without null checking (lines 80-88, 95). If part.data is null—which past comments indicate can happen mid-stream—the component will crash when accessing toolData.status, toolData.approval, or toolData.toolcallid.

  2. userApprovalOverride never resets when the tool call changes. If the component instance is reused for a different tool call, the stale override persists, potentially showing "denied" for a brand-new tool and stopping keep-alive emissions.

Based on past review comments

Apply this diff to add null safety and reset logic:

 const AIToolUse = memo(({ part }: AIToolUseProps) => {
     const toolData = part.data;
     const [userApprovalOverride, setUserApprovalOverride] = useState<string | null>(null);
+    const toolCallId = toolData?.toolcallid;
+    const effectiveApproval = userApprovalOverride ?? toolData?.approval ?? "";
 
-    const statusIcon = toolData.status === "completed" ? "✓" : toolData.status === "error" ? "✗" : "•";
-    const statusColor =
-        toolData.status === "completed"
-            ? "text-green-400"
-            : toolData.status === "error"
-              ? "text-red-400"
-              : "text-gray-400";
-
-    const effectiveApproval = userApprovalOverride || toolData.approval;
-
     useEffect(() => {
-        if (effectiveApproval !== "needs-approval") return;
+        if (!toolCallId || effectiveApproval !== "needs-approval") {
+            return;
+        }
 
         const interval = setInterval(() => {
             RpcApi.WaveAIToolApproveCommand(TabRpcClient, {
-                toolcallid: toolData.toolcallid,
+                toolcallid: toolCallId,
                 keepalive: true,
             });
         }, 4000);
 
         return () => clearInterval(interval);
-    }, [effectiveApproval, toolData.toolcallid]);
+    }, [effectiveApproval, toolCallId]);
+
+    useEffect(() => {
+        setUserApprovalOverride(null);
+    }, [toolCallId, toolData?.approval]);
+
+    if (!toolData) {
+        return null;
+    }
+
+    const statusIcon = toolData.status === "completed" ? "✓" : toolData.status === "error" ? "✗" : "•";
+    const statusColor =
+        toolData.status === "completed"
+            ? "text-green-400"
+            : toolData.status === "error"
+              ? "text-red-400"
+              : "text-gray-400";
🧹 Nitpick comments (1)
frontend/app/aipanel/aimessage.tsx (1)

73-73: Remove unused prop.

The isStreaming prop is declared but never used in the component.

Apply this diff to remove the unused prop:

 interface AIToolUseProps {
     part: WaveUIMessagePart & { type: "data-tooluse" };
-    isStreaming: boolean;
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1a28f77 and b6e74ee.

📒 Files selected for processing (3)
  • frontend/app/aipanel/aimessage.tsx (5 hunks)
  • frontend/app/tab/tabbar.tsx (3 hunks)
  • pkg/aiusechat/tools_readfile.go (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
frontend/app/aipanel/aimessage.tsx (3)
frontend/app/aipanel/aitypes.ts (1)
  • WaveUIMessagePart (24-24)
frontend/app/store/wshclientapi.ts (1)
  • RpcApi (537-537)
frontend/app/store/wshrpcutil.ts (1)
  • TabRpcClient (37-37)
pkg/aiusechat/tools_readfile.go (2)
pkg/util/utilfn/marshal.go (1)
  • ReUnmarshal (34-40)
pkg/aiusechat/uctypes/usechat-types.go (1)
  • ApprovalNeedsApproval (129-129)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Analyze (go)
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Build for TestDriver.ai
🔇 Additional comments (6)
frontend/app/tab/tabbar.tsx (2)

9-9: LGTM! Dev-mode gate removed as intended.

The removal of isDev from the imports aligns with making the AI button always visible, promoting the AI features from development-only to general availability.


643-652: LGTM! AI button now always visible.

The button implementation is clean and correct. The conditional styling based on aiPanelOpen provides clear visual feedback, and the click handler properly toggles the AI panel. This change makes AI features generally available to all users.

pkg/aiusechat/tools_readfile.go (4)

106-124: LGTM: Path expansion and validation.

The home directory expansion via wavebase.ExpandHomeDir is correctly applied to all file operations. The directory check (lines 116-118) provides helpful guidance to users about using the read_dir tool instead.


198-198: LGTM: File mode in response.

Adding the file mode to the response provides useful metadata about the file's permissions.


211-211: LGTM: Description documents approval requirement.

The description update correctly informs users that this tool requires approval, aligning with the new ToolApproval function.


280-282: LGTM: Tool approval implementation.

The ToolApproval function correctly returns uctypes.ApprovalNeedsApproval, enabling the approval flow for this tool as described in the PR objectives.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (3)
pkg/aiusechat/tools_readfile.go (3)

28-29: Extract duplicate constant to package level.

DefaultLineCount is defined in both parseReadTextFileInput (line 29) and readTextFileCallback (line 100), creating a maintenance issue if the default value needs to change.

Apply this diff to define the constant once at package level:

 package aiusechat

 import (
 	"fmt"
 	"io"
 	"os"
 	"strings"

 	"github.com/wavetermdev/waveterm/pkg/aiusechat/uctypes"
 	"github.com/wavetermdev/waveterm/pkg/util/readutil"
 	"github.com/wavetermdev/waveterm/pkg/util/utilfn"
 	"github.com/wavetermdev/waveterm/pkg/wavebase"
 )

+const DefaultLineCount = 100
 const StopReasonMaxBytes = "max_bytes"

Then remove the duplicate definitions from both functions:

 func parseReadTextFileInput(input any) (*readTextFileParams, error) {
-	const DefaultLineCount = 100
-
 	result := &readTextFileParams{}
 func readTextFileCallback(input any) (any, error) {
-	const DefaultLineCount = 100
 	const DefaultMaxBytes = 50 * 1024

28-73: Past review comment remains unaddressed: readTextFileCallback should use this function.

The past review comment suggested refactoring readTextFileCallback to use parseReadTextFileInput internally to eliminate duplication. Currently, readTextFileCallback (lines 99-217) still contains its own validation logic, duplicating the work done here.

Consider refactoring readTextFileCallback to use this function:

 func readTextFileCallback(input any) (any, error) {
 	const DefaultMaxBytes = 50 * 1024
 	const ReadLimit = 1024 * 1024 * 1024

-	var params readTextFileParams
-	if err := utilfn.ReUnmarshal(&params, input); err != nil {
-		return nil, fmt.Errorf("invalid input format: %w", err)
+	params, err := parseReadTextFileInput(input)
+	if err != nil {
+		return nil, err
 	}

-	if params.Filename == "" {
-		return nil, fmt.Errorf("missing filename parameter")
-	}
-
 	maxBytes := DefaultMaxBytes
 	if params.MaxBytes != nil {
 		maxBytes = *params.MaxBytes
 	}

Then remove the duplicate validation logic from lines 152-176 since parseReadTextFileInput already handles it.


261-290: Replace hardcoded values with DefaultLineCount constant.

Lines 275 and 280 use the hardcoded value 100 instead of the DefaultLineCount constant, creating a maintenance issue if the default changes. This was flagged in a previous review comment.

Apply this diff to use the constant:

 		ToolInputDesc: func(input any) string {
 			parsed, err := parseReadTextFileInput(input)
 			if err != nil {
 				return fmt.Sprintf("error parsing input: %v", err)
 			}

 			origin := "start"
 			if parsed.Origin != nil {
 				origin = *parsed.Origin
 			}
 			offset := 0
 			if parsed.Offset != nil {
 				offset = *parsed.Offset
 			}
-			count := 100
+			count := DefaultLineCount
 			if parsed.Count != nil {
 				count = *parsed.Count
 			}

-			if origin == "start" && offset == 0 {
+			if origin == "start" && offset == 0 && count == DefaultLineCount {
 				return fmt.Sprintf("reading %q (first %d lines)", parsed.Filename, count)
 			}
 			if origin == "end" && offset == 0 {
 				return fmt.Sprintf("reading %q (last %d lines)", parsed.Filename, count)
 			}
 			if origin == "end" {
 				return fmt.Sprintf("reading %q (from end: offset %d lines, count %d lines)", parsed.Filename, offset, count)
 			}
 			return fmt.Sprintf("reading %q (from start: offset %d lines, count %d lines)", parsed.Filename, offset, count)
 		},
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b6e74ee and f70f7fd.

📒 Files selected for processing (1)
  • pkg/aiusechat/tools_readfile.go (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
pkg/aiusechat/tools_readfile.go (2)
pkg/util/utilfn/marshal.go (1)
  • ReUnmarshal (34-40)
pkg/aiusechat/uctypes/usechat-types.go (1)
  • ApprovalNeedsApproval (129-129)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Build for TestDriver.ai
  • GitHub Check: Analyze (go)
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (2)
pkg/aiusechat/tools_readfile.go (2)

118-136: LGTM! Excellent path handling and early directory detection.

The path expansion logic and early directory detection improve usability and provide clear error messages guiding users to the appropriate tool.


292-294: LGTM! Approval function correctly implemented.

The ToolApproval function correctly returns ApprovalNeedsApproval, aligning with the PR's objective to add tool approval support.

Comment on lines +59 to +61
if *result.Offset < 0 {
return nil, fmt.Errorf("offset must be non-negative, got %d", *result.Offset)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Inconsistent offset validation: returns error here but normalizes in readTextFileCallback.

parseReadTextFileInput returns an error for negative offsets, but readTextFileCallback (lines 174-176) normalizes negative offsets to 0. This inconsistency can cause confusion.

Since the past review comment suggested that readTextFileCallback should use parseReadTextFileInput internally, decide on one approach:

  • Option 1 (Recommended): Normalize negative offsets to 0 in both places
  • Option 2: Return an error for negative offsets in both places

If choosing Option 1, apply this diff to align the behavior:

 	if *result.Offset < 0 {
-		return nil, fmt.Errorf("offset must be non-negative, got %d", *result.Offset)
+		offset := 0
+		result.Offset = &offset
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if *result.Offset < 0 {
return nil, fmt.Errorf("offset must be non-negative, got %d", *result.Offset)
}
if *result.Offset < 0 {
offset := 0
result.Offset = &offset
}
🤖 Prompt for AI Agents
In pkg/aiusechat/tools_readfile.go around lines 59-61, parseReadTextFileInput
currently returns an error for negative offsets while readTextFileCallback
normalizes negatives to 0, causing inconsistent behavior; change
parseReadTextFileInput to normalize negative offsets to 0 instead of returning
an error by setting result.Offset = pointerTo(0) (or equivalent) and update any
associated validations/messages accordingly so both functions treat negative
offsets the same (normalized to 0).

@sawka sawka merged commit fd0e75a into main Oct 9, 2025
7 of 8 checks passed
@sawka sawka deleted the sawka/tool-approval branch October 9, 2025 17:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant